//Copyright 2008 Kristian Gundry And Rune Juhl-Petersen
//This file is part of Plain.

//Plain is free software: you can redistribute it and/or modify
//it under the terms of the GNU Lesser General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//any later version.

//Plain is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU Lesser General Public License for more details.

//You should have received a copy of the GNU Lesser General Public License
//along with Plain.  If not, see <http://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Reflection;
using System.IO;
using Plain.Search;
using System.Collections;
using System.Xml.XPath;
using Plain.Data;
using Plain;

namespace Plain.Data.Xml {
    /// <summary>
    /// Date Access Object to persist to simple xml
    /// </summary>
    /// <typeparam name="T">Specifies the type (T), the dao can handle</typeparam>
    /// <typeparam name="TT">Specifies the type (TT), of the identifier for the type T</typeparam>
    public class XmlDao<T, TT> : IDao<T, TT>, ISearchableDao<T, TT> where T : IBusinessEntity<TT>, new() {
        #region Fields

        private static XmlDocument doc = null;
        private static string savePath = null;

        #endregion

        #region Contructors

        /// <summary>
        /// Default contructor        
        /// </summary>
        private XmlDao() {
            if (typeof(TT) != typeof(int)) {
                throw new Exception("Id must be int to persist as xml");
            }
        }

        /// <summary>
        /// Contructor
        /// </summary>
        /// <param name="path">path to xml file used to load and save data</param>
        public XmlDao(string path)
            : this() {
            if (doc == null) {
                doc = new XmlDocument();
                savePath = path;
                if (!File.Exists(path)) {
                    doc.LoadXml("<Plain></Plain>");
                    doc.Save(path);
                } else {
                    doc.Load(path);
                }
            }
        }

        #endregion

        #region Properties

        #endregion

        #region IDao<T,int> Members

        /// <summary>
        /// Saves entity to XML
        /// </summary>
        /// <param name="entity">The entity to save</param>
        /// <returns>The identifier of the entity</returns>
        public TT Save(T entity) {
            if (entity.Id.Equals(default(TT))) {
                lock (doc) {
                    string nodeName = typeof(T).Name;
                    XmlNode groupNode = doc.SelectSingleNode("/Plain/" + nodeName + "s");
                    if (groupNode == null) {
                        groupNode = doc.CreateElement(typeof(T).Name + "s");
                        doc.DocumentElement.AppendChild(groupNode);
                    }
                    XmlNode node = doc.CreateElement(typeof(T).Name);
                    int? id = doc.SelectNodes("Plain/" + typeof(T).Name + "s/" + nodeName).Count + 1;
                    node.Attributes.Append(doc.CreateAttribute("id")).InnerText = (id).ToString();
                    oToXml(entity, node);
                    groupNode.AppendChild(node);
                    doc.Save(savePath);
                    PropertyInfo fi = entity.GetType().GetProperty("Id");
                    fi.SetValue(entity, id, null);
                    //return id?

                    return entity.Id;
                }
            } else {
                lock (doc) {
                    string nodeName = typeof(T).Name;
                    XmlNode node = doc.SelectSingleNode("/Plain/" + nodeName + "s/" + nodeName + "[@id='" + entity.Id + "']");
                    oToXml(entity, node);
                    doc.Save(savePath);
                    return entity.Id;
                }
            }
        }

        /// <summary>
        /// Gets Entity from XML
        /// </summary>
        /// <param name="id">The id of the entity to get</param>
        /// <returns>The entity</returns>
        public T Get(TT id) {

            string xPath = "/Plain/" + typeof(T).Name + "s/" + typeof(T).Name + "[@id='" + id.ToString() + "']";
            return xmlToO(doc.SelectSingleNode(xPath));
        }

        /// <summary>
        /// Returns all entities from XML
        /// </summary>
        /// <returns>A list of all the entities</returns>
        public IList<T> GetAll() {
            string nodeName = typeof(T).Name;
            IList<T> list = new List<T>();
            //Go through all the XML nodes and create domain objects
            foreach (XmlNode node in doc.SelectNodes("/Plain/" + nodeName + "s/" + nodeName)) {
                list.Add(xmlToO(node));
            }
            return list;
        }

        /// <summary>
        /// Deletes a entity
        /// </summary>
        /// <param name="entity">The entity to delete</param>
        public void Delete(T entity) {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Serialize an entity to xml
        /// </summary>
        /// <param name="entity">the entity to serialize</param>
        /// <param name="node">The node the entity is serialized to</param>
        private void oToXml(T entity, XmlNode node) {
            foreach (PropertyInfo prop in entity.GetType().GetProperties()) {
                if (prop.Name != "Id") {
                    XmlNode xProp = node.SelectSingleNode(prop.Name);
                    object o = prop.GetValue(entity, null);
                    if (o != null) {
                        if (xProp == null) {
                            xProp = node.AppendChild(doc.CreateElement(prop.Name));
                        }

                        Type[] interfaces = prop.PropertyType.GetInterfaces();
                        bool found = false;
                        foreach (Type type in interfaces) {
                            if (type == typeof(IBusinessEntity<>)) {
                                found = true;
                                break;
                            }
                        }
                        if (found) {
                            IBusinessEntity<int> ent = prop.GetValue(entity, null) as IBusinessEntity<int>;
                            if (ent != null) {
                                xProp.InnerText = ent.Id.ToString();
                            } else {
                                xProp.InnerText = "";
                            }

                        } else if (prop.PropertyType is IEnumerable) {
                            IEnumerable en = (IEnumerable)prop.GetValue(entity, null);
                            xProp.RemoveAll();
                            foreach (object item in en) {
                                if (item.GetType().IsValueType) {
                                    XmlNode itemProp = node.AppendChild(doc.CreateElement("Item"));
                                    itemProp.InnerText = item.ToString();
                                    //create new item and put value
                                } else if (item.GetType() == typeof(IBusinessEntity<int>)) {
                                    //save xml entity
                                    //save id in Item
                                }

                            }
                        } else {
                            xProp.InnerText = prop.GetValue(entity, null).ToString();
                        }
                    }

                }
            }
        }

        /// <summary>
        /// Deserialize the data in the <see cref="XmlNode"/> to a new object specified by <typeparam name="T"/>
        /// </summary>
        /// <param name="node">The Xmlnode to deserialize</param>
        /// <returns>The entity specified by <typeparam name="T"/></returns>
        private T xmlToO(XmlNode node) {
            return xmlToOgen<T>(node);
        }

        #endregion

        #region ISearchableDao<T,int> Members

		
		public IList<T> Search(Plain.Search.Criteria crit){
			return Search(crit,new Dictionary<string,object>(),0,0,"");
		}
		
        public IList<T> Search(Plain.Search.Criteria crit, IDictionary<string, object> parameters, long start, long end, string order) {

            XPathBuilder<T> builder = new XPathBuilder<T>(parameters);

            string nodeName = typeof(T).Name;
            IList<T> list = new List<T>();
            //Go through all the XML nodes and create domain objects
            //string h = hql(crit);
            string xPathQuery;
            if (crit != null) {
                xPathQuery = "/Plain/" + nodeName + "s/" + nodeName + "[" + builder.xPath(crit) + "]";
            } else {
                xPathQuery = "/Plain/" + nodeName + "s/" + nodeName;
            }
            XPathNavigator nav = doc.CreateNavigator();




            XPathExpression expression = nav.Compile(xPathQuery);
            if (!string.IsNullOrEmpty(order)) {
                string[] s = order.Split(' ');
                XmlSortOrder sortorder = XmlSortOrder.Ascending;
                if (s.Length == 2 && s[1].ToLower() == "desc") {
                    sortorder = XmlSortOrder.Descending;
                }
                string s1 = "//Categorys/Category[@id=//Categorys/Category[@id=node()/Category]/SubCategory]/Name";
                //expression.AddSort(s[0], sortorder, XmlCaseOrder.None, string.Empty, XmlDataType.Text);
                string[] path = s[0].Split('.');
                if (path.Length > 1) {
                    Type t;
                    string so = parseMultiLevelSort(path, 0, typeof(T).GetProperty(path[0]).PropertyType, "node()/" + path[0], out t);
                    expression.AddSort(so, sortorder, XmlCaseOrder.None, string.Empty, XmlDataType.Text);
                } else {
                    expression.AddSort(s1, sortorder, XmlCaseOrder.None, string.Empty, XmlDataType.Text);
                }

            }


            XPathNodeIterator iterator = nav.Select(expression);
            int count = 0;
            foreach (XPathNavigator n in iterator) {
                if (count++ >= start) {
                    n.MoveToFirstAttribute();
                    string id = n.Value;
                    xPathQuery = "/Plain/" + nodeName + "s/" + nodeName + "[@id='" + id + "']";
                    XmlNode node = doc.SelectSingleNode(xPathQuery);
                    list.Add(xmlToO(node));
                }
                if (end > 0 && count >= start + end) {
                    break;
                }
            }

            //for (long i = start; i < start + end; i++) {
            //    XmlNode node = nodes.Item((int)i);
            //    if (node != null) {
            //        list.Add(xmlToO(node));
            //    } else {
            //        break;
            //    }
            //}


            return list;
        }
        public int GetCount(Criteria crit, IDictionary<string, object> parameters) {
            string nodeName = typeof(T).Name;
            XPathBuilder<T> builder = new XPathBuilder<T>(parameters);
            string xPathQuery;
            if (crit != null) {
                xPathQuery = "/Plain/" + nodeName + "s/" + nodeName + "[" + builder.xPath(crit) + "]";
            } else {
                xPathQuery = "/Plain/" + nodeName + "s/" + nodeName;
            }
            XmlNodeList nodes = doc.SelectNodes(xPathQuery);
            if (nodes != null) {
                return nodes.Count;
            } else {
                return 0;
            }

            //return list;
        }








        #endregion

        #region Methods

        /// <summary>
        /// Deserialize the data in the <see cref="XmlNode"/> to a new object specified by <typeparam name="P"/>
        /// </summary>
        /// <typeparam name="p">The type of the entity to deserialize</typeparam>
        /// <param name="node">The Xmlnode to deserialize</param>
        /// <returns>The entity specified by <typeparam name="P"/></returns>
        public P xmlToOgen<P>(XmlNode node) where P : new() {
            if (node != null) {
                P entity = new P();

                //Get next ID
                int id = (int)Convert.ChangeType(node.Attributes["id"].Value, typeof(int));
                //Set Id on businessEntity
                PropertyInfo fi = entity.GetType().GetProperty("Id");
                fi.SetValue(entity, id, null);

                foreach (PropertyInfo prop in entity.GetType().GetProperties()) {
                    if (prop != fi) {
                        XmlNode xProp = node.SelectSingleNode(prop.Name);
                        if (xProp != null) {
                            Type[] interfaces = prop.PropertyType.GetInterfaces();
                            bool found = false;
                            foreach (Type type in interfaces) {
                                if (type == typeof(IBusinessEntity<>)) {
                                    found = true;
                                    break;
                                }
                            }
                            if (found) {
                                Type subType = prop.PropertyType;
                                string xPath = "/Plain/" + subType.Name + "s/" + subType.Name + "[@id='" + xProp.InnerText + "']";
                                XmlNode subNode = doc.SelectSingleNode(xPath);
                                MethodInfo m = GetType().GetMethod("xmlToOgen");
                                MethodInfo g = m.MakeGenericMethod(prop.PropertyType);
                                object p = g.Invoke(this, new object[] { subNode });
                                prop.SetValue(entity, p, null);
                            } else {
                                bool isEnumerable = false;
                                foreach (Type iType in prop.PropertyType.GetInterfaces()) {
                                    if (iType == typeof(IEnumerable)) {
                                        isEnumerable = true;
                                        break;
                                    }
                                }
                                if (!isEnumerable || prop.PropertyType == typeof(String)) {
                                    prop.SetValue(entity, Convert.ChangeType(xProp.InnerText, prop.PropertyType), null);
                                } else {
                                    object a = Activator.CreateInstance(prop.PropertyType);
                                    prop.SetValue(entity, a, null);
                                    MethodInfo m = a.GetType().GetMethod("Add");
                                    Type t = a.GetType().GetGenericArguments()[0];
                                    bool listfound = false;
                                    foreach (Type type in t.GetInterfaces()) {
                                        if (type == typeof(IBusinessEntity<>)) {
                                            listfound = true;
                                            break;
                                        }
                                    }
                                    foreach (XmlNode node2 in xProp.SelectNodes("Item")) {

                                        //if ibusinessEntity
                                        if (listfound) {
                                            string xPath = "/Plain/" + t.Name + "s/" + t.Name + "[@id='" + node2.InnerText + "']";
                                            XmlNode subNode = doc.SelectSingleNode(xPath);
                                            MethodInfo m2 = GetType().GetMethod("xmlToOgen");
                                            MethodInfo g2 = m2.MakeGenericMethod(t);
                                            object p = g2.Invoke(this, new object[] { subNode });
                                            m.Invoke(a, new object[] { p });
                                        }
                                            //if 
                                        else {
                                            m.Invoke(a, new object[] { Convert.ChangeType(node2.InnerText, t) });
                                        }
                                    }
                                }
                                //a.GetType().GenericParameterAttributes;
                            }
                        }
                    }
                }
                return entity;
            } else return default(P);

        }




        private string parseMultiLevelSort(string[] ids, int index, Type currentType, string q, out Type type) {

            string p = "//{0}s/{0}[@id={1}]/{2}";
            string ps = string.Format(p, currentType.Name, q, ids[index + 1]);

            if (index < ids.Length - 2) {

                PropertyInfo prop = currentType.GetProperty(ids[index]);
                return parseMultiLevelSort(ids, index + 1, prop.PropertyType, ps, out type);
            } else {
                type = currentType;
                return ps;

                //return "";//string.Format("{0} = //{1}[{2}]/@id", ids[index], prop.PropertyType.Name, string.Format(c, ids[index + 1], payload.ToString()));

            }
        }



        #endregion
    }
    public class XPathBuilder<T> {
        IDictionary<string, object> parameters = null;
        public XPathBuilder(IDictionary<string, object> parameters) {
            this.parameters = parameters;
        }
        /// <summary>
        /// Creates an xPath from a <see cref="Criteria"/>
        /// </summary>
        /// <param name="crit">The <see cref="Criteria"/> to create the xPath from</param>
        /// <returns>string xPath</returns>
        public string xPath(Criteria crit) {
            CriteriaCollection col = crit as CriteriaCollection;
            if (col != null) {
                return xPath(col);
            } else {
                return handleCriteria(crit);
            }
        }
        /// <summary>
        /// Creates an xPath from a <see cref="CriteriaCollection"/>
        /// </summary>
        /// <param name="col">The <see cref="CriteriaCollection"/> to create the xPath from </param>
        /// <returns>string xPath</returns>
        private string xPath(CriteriaCollection col) {
            StringBuilder sb = new StringBuilder();
            foreach (Criteria crit in col.Criterias) {
                string r = xPath(crit);
                if (!string.IsNullOrEmpty(r)) {
                    if (sb.Length > 0) {
                        sb.Append(" " + parseCollectionType(col.Type) + " ");
                    }
                    sb.Append(r);
                }
            }
            if (!string.IsNullOrEmpty(sb.ToString())) {
                if (col.Type == CriteriaCollectionType.OneOf) {
                    return "(" + sb.ToString() + ")";
                } else {
                    return sb.ToString();
                }
            } else return null;
        }
        /// <summary>
        /// Creates a single xPath from a <see cref="Criteria"/>
        /// </summary>
        /// <param name="crit"><see cref="Criteria"/> to create XPath from</param>
        /// <returns>string xPath</returns>
        private string handleCriteria(Criteria crit) {
            SimpleCriteria simp = crit as SimpleCriteria;
            if (simp != null) {
                string c;
                switch (simp.Type) {
                    case SimpleCriteriaType.Equal:
                        c = "{0}='{1}'";
                        break;
                    case SimpleCriteriaType.GreaterThan:
                        c = "{0}&gt;'{1}'";
                        break;
                    case SimpleCriteriaType.LessThan:
                        c = "{0}&lt;'{1}'";
                        break;
                    case SimpleCriteriaType.NotEqual:
                        c = "{0}!='{1}'";
                        break;
                    case SimpleCriteriaType.Contains:
                        c = "contains({0},'{1}')";
                        break;
                    case SimpleCriteriaType.StartsWith:
                        c = "starts-with({0},'{1}')";
                        break;
                    case SimpleCriteriaType.EndsWith:
                        c = "ends-with({0},'{1}')";
                        break;
                    default:
                        c = "0";
                        break;
                }
                string[] splits = simp.Id.Name.Split('.');

                object val = "";
                if (parameters != null && parameters.ContainsKey(simp.ValueId)) {
                    val = parameters[simp.ValueId];
                } else {
                    val = simp.Payload;
                }


                if (splits.Length == 1) {
                    return string.Format(c, simp.Id.Name, val.ToString());
                } else if (splits.Length > 1) {
                    return parseMultiLevelQuery(splits, 0, typeof(T), c, val);
                } else {
                    return "true";
                    //throw new Exception("XmlDao only supports 2 levels of objects in search");
                }
            } else throw new Exception("Unknown criteria type");
        }
        /// <summary>
        /// <see cref="CriteriaCollection"/> separater 
        /// </summary>
        /// <param name="type">the <see cref="CriteriaCollectionType"/></param>
        /// <returns>string separator("and" or "or")</returns>
        private string parseCollectionType(CriteriaCollectionType type) {
            switch (type) {
                case CriteriaCollectionType.OneOf:
                    return "or";

                default:
                    return "and";
            }
        }
        /// <summary>
        /// Creates a xPath to handle multiple levels of entities
        /// eg. Article.Category.Name == "Rune"
        /// </summary>
        /// <param name="ids">ids of the sublevel entities</param>
        /// <param name="index">the level of the entity</param>
        /// <param name="currentType">the type of the entity</param>
        /// <param name="c">the xPath criteria</param>
        /// <param name="payload">The value to compare</param>
        /// <returns>string xPath</returns>
        private string parseMultiLevelQuery(string[] ids, int index, Type currentType, string c, object payload) {
            if (index < ids.Length - 2) {
                PropertyInfo prop = currentType.GetProperty(ids[index]);
                return string.Format("{0} = //{1}[{2}]/@id", ids[index], prop.PropertyType.Name, parseMultiLevelQuery(ids, index + 1, prop.PropertyType, c, payload));

            } else {
                PropertyInfo prop = currentType.GetProperty(ids[index]);
                return string.Format("{0} = //{1}[{2}]/@id", ids[index], prop.PropertyType.Name, string.Format(c, ids[index + 1], payload.ToString()));

            }
        }
    }
}